/*** Created Time: 2022年5月13日 19:31:19 ***/
/*** Author: Eric                        ***/

#include <onix/stdarg.h>
#include <onix/string.h>
#include <onix/assert.h>
#include <onix/stdio.h>

// 源码参考自 linux/lib/vsprintf.c

// 标志位宏定义
#define     ZEROPAD     (1)   // 填充数字零
#define     SIGN        (2)   // unsigned/signed long
#define     PLUS        (4)   // 显示'+'符号
#define     SPACE       (8)   // 如果是'+'，则填充空格
#define     LEFT        (16)  // 左对齐
#define     SPECIAL     (32)  // 特殊转换，表示是否存在十六进制或者八进制结果
#define     SMALL       (64)  // 使用小写字母集

// 判断输入字符是否是数字的宏实现，<ctype.h>
#define     is_digit(c)     ((c) >= '0' && (c) <= '9')

// 辅助函数声明
static int skip_atoi(const char **s);
static char *number(char *str, unsigned long num, int base, int size, int precision, int flags);


// 辅助函数，将字符数字串转换为整数，并将指针向前移动
// @s : 指向字符串s的二维指针
// 输出 : 转换后的十进制数字
static int skip_atoi(const char **s)
{
    int num = 0;

    while(is_digit(**s)) {
        num = num*10 + *((*s)++) - '0';
    }

    return num;
}

// 辅助函数，将整数转换为指定进制的字符串
// @str : 输出字符串指针
// @num : 整数
// @base : 进制基数
// @size : 字符串的宽度（长度）
// @precision : 精度
// @flags : 选项
static char *number(char *str, unsigned long num, int base, int size, int precision, int flags)
{
    char c, sign, tmp[36];  // c : 数字'0'字符或者空格' '字符
                            // sign : 符号位
                            // tmp : 临时字符串缓冲区
    // 定义大写字母数字集，digits在内存的位置不可改变，但该内存存储的内容可变
    const char *digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";    
    int i;
    int index;
    char *ptr = str;    // 输出字符串缓冲区

    // 如果flags指示使用小写字母数字集，则定义小写字母集
    if(flags & SMALL) {
        digits = "0123456789abcdefghijklmnopqrstuvwxyz";
    }

    // 如果flags指示左对齐，则屏蔽类型中的填充数字零标志
    if(flags & LEFT) {
        flags &= ~ZEROPAD;
    }
    
    // 如果进制基数base小于2或者大于36，则退出处理
    // 本函数只能处理基数在2~32之间的数
    if(base<2 || base>36) {
        return 0;
    }

    // 如果flags指示填充数字零标志，则设置字符变量c='0'，否则c=' '，即空格字符
    c = (flags & ZEROPAD) ? '0' : ' ';

    // 如果flags指示num是有符号数且num小于0，设置符号变量sign=负号，并使num取绝对值
    if((flags & SIGN) && num<0) {
        sign = '-';
        num = -num;
    } else {
        // 判断flags是否指示PLUS（即加号标志），为真则sign=加号
        // 否则判断flags是否指示空格表示，为真则sign=空格，否则sign=0 (ASCII的0，即NUL)
        sign = (flags & PLUS) ? '+' : ((flags & SPACE) ? ' ' : 0);
    }

    // 若输出字符串带符号，则字符串宽度减一
    if(sign) {
        --size;
    }

    // 如果flags指示存在特殊转换，则对于十六进制，输出字符串宽度再减少两位（用于0x）
    if(flags & SPECIAL) {
        if(base == 16) {
            size -= 2;
        } else if(base == 8) {
            // 对于八进制，输出字符串宽度减少一位（用于前导符数字0）
            size -= 1;
        }
    }

    i = 0;
    // 如果数值num为0，则临时缓冲区tmp='0'，否则根据给定的基数将num转换成字符形式
    // 注意tmp此时存储的是倒序数字
    if(num == 0) {
        tmp[i++] = '0';
    } else {
        while(num != 0) {
            index = num % base;
            num /= base;
            tmp[i++] = digits[index];
        }
    }

    // 如果数值字符个数大于给定精度值precision，则扩展精度值为当前数值字符个数
    if(i > precision) {
        precision = i;
    }

    // 宽度值size减去用于存放数值字符的个数（精度），即求出空余宽度
    size -= precision;

    // 从这里真正开始形成所需要的转换结果，并将结果放在字符串str中

    // 若flags中没有设置零填充(ZEROPAD)和左对齐(LEFT)标志
    // 则在str中首先填充剩余宽度值指出的空格数量
    if(!(flags & (ZEROPAD | LEFT))) {
        while(size-- > 0) {
            *str++ = ' ';
        }
    }

    // 如果需要添加符号位，则填入符号
    if(sign) {
        *str++ = sign;
    }

    // 如果flags设置为特殊转换（十六进制或者八进制）
    if(flags & SPECIAL) {
        // 对于八进制转换，第一位放置前导符数字0
        if(base == 8) {
            *str++ = '0';
        } else if(base == 16) {
            // 对于十六进制，放置'0x'
            *str++ = '0';
            *str++ = digits[33];    // 'x'
        }
    }

    // 如果flags没有设置左对齐标志，则剩余宽度中存放c字符（空格字符或'0'字符，取决于是否设置零填充标志）
    if(!(flags & LEFT)) {
        while(size-- > 0) {
            *str++ = c;
        }
    }

    // 此时i存有数值num的数字个数（长度）

    // 若数字个数小于精度值，则str中放入（精度值-i）个'0'
    while(i < precision--) {
        *str++ = '0';
    }

    // 将转换好的数字字符从tmp填入str中，tmp中存储的是倒序数字，再倒序写入变成正序
    while(i-- > 0) {
        *str++ = tmp[i];
    }

    // 如果宽度size仍然大于零，则表示flags设置了左对齐标志
    // 因此在剩余宽度中放入空格字符
    while(size-- > 0) {
        *str++ = ' ';
    }

    return str;
}

// 将args按格式fmt写入字符串buf中，存在内存溢出风险
// 正常情况下返回生成字符串的长度(除去\0)，错误情况返回负值
// @buf : 结果缓冲区
// @fmt : 格式
// @args : 可变参数列表
int vsprintf(char *buf, const char *fmt, va_list args)
{
    int len;
    int i;

    // 用于存放转换过程中的字符串
    char *str;
    char *s;
    int *ip;

    // number()函数使用的标志位flags
    int flags;

    int field_width;    // 输出字段宽度
    int precision;      // min 整数数字个数；max 字符串中字符个数
    int qualifier;      // 'h', 'l' 或 'L' 用于整数字段

    // 首先将字符指针指向buf，然后扫描格式字符串，并对各个格式转换指示符进行处理
    for(str=buf; *fmt; ++fmt) {
        // 格式转换指示字符串均以'%'开始
        // 从 fmt 格式字符串中扫描'%'，寻找格式转换字符串的开始
        // 不是格式指示的其他字符均被依次存入str
        if(*fmt != '%') {
            *str++ = *fmt;
            continue;
        }

        // 下面取得格式指示字符串中的标志域，并将标志常量放入flags变量中
        flags = 0;
    repeat:
        ++fmt;  // 跳过第一个'%'
        switch (*fmt) {
        // 左对齐
        case '-':
            flags |= LEFT;
            goto repeat;
        // 放加号
        case '+':
            flags |= PLUS;
            goto repeat;
        // 放空格
        case ' ':
            flags |= SPACE;
            goto repeat;
        // 特殊转换
        case '#':
            flags |= SPECIAL;
            goto repeat;
        // 填充零，否则是空格
        case '0':
            flags |= ZEROPAD;
            goto repeat;
        // 当上述条件皆不满足时，表示flags标志设置完成，跳出switch
        }

        // 取当前参数字段宽度域值，放入field_width变量中（若存在）
        field_width = -1;

        // 如果宽度域中是数值则直接取其为宽度值
        if(is_digit(*fmt)) {
            field_width = skip_atoi(&fmt);
        } else if(*fmt == '*') {
            // 如果宽度域中是字符'*'，表示下一个参数指定宽度
            // 当使用*号后，表示精度（宽度）都从后面的参数表中取出
            ++fmt;
            // 调用va_arg取宽度值
            field_width = va_arg(args, int);

            // 若此时宽度值小于0，表示其带有'-'标志，指示左对齐
            if(field_width < 0) {
                // 因此还需在标志变量中添入该标志，并将字段宽度值取为其绝对值
                field_width = -field_width;
                flags |= LEFT;
            }
        }

        // 取格式转换串的精度域，并放入 precision 变量中（若存在）
        precision = -1;

        // 精度域开始的标志是'.'，处理过程与上面宽度域的类似
        if(*fmt == '.') {
            ++fmt;
            // 如果精度域中是数值则直接取其为精度值
            if(is_digit(*fmt)) {
                precision = skip_atoi(&fmt);
            } else if(*fmt == '*') {
                // 如果精度域中是字符'*'，表示下一个参数指定精度
                // 当使用*号后，表示精度（宽度）都从后面的参数表中取出
                ++fmt;
                // 调用va_arg取精度值
                precision = va_arg(args, int);
            }

            // 若此时精度值小于0，则将字段精度值0
            if(precision < 0) {
                precision = 0;
            }
        }

        // 下面分析长度修饰符，若存在则将其存入qualifier变量
        qualifier = -1;
        if(*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
            qualifier = *fmt;
            ++fmt;
        }

        // 下面分析转换指示符
        switch (*fmt) {
        // 如果转换指示符是'c'，则表示对应参数应是字符类型
        case 'c':
            // 此时如果标志域表明不是左对齐
            if(!(flags & LEFT)) {
                // 则该字段前面放入 (宽度域值 - 1) 个空格字符，然后再放入参数字符
                while(--field_width > 0) {
                    *str++ = ' ';
                }
            }
            
            *str++ = (unsigned char)va_arg(args, int);
            // 如果宽度域还大于0，则表示为左对齐,则在参数字符后面添加 (宽度值-1) 个空格字符
            while(--field_width > 0) {
                *str++ = ' ';
            }

            break;
        // 如果转换指示符是 's'，则表示对应参数是字符串
        case 's':
            s = va_arg(args, char *);
            // 首先取参数字符串的长度
            len = strlen(s);
            // 如果其长度超过了精度域值，则重新设置长度为精度域值
            if(precision < 0) {
                precision = len;
            } else if(len > precision) {
                precision = len;
            }
            // 如果flags指示不是左对齐
            if(!(flags & LEFT)) {
                // 则该字段前放入 (宽度值-字符串长度) 个空格字符
                while(len < field_width--) {
                    *str++ = ' ';
                }
            }
            // 然后再放入参数字符串
            for(i=0; i<len; ++i) {
                *str++ = *s++;
            }
            // 如果宽度域还大于 0，则表示为左对齐
            // 则在参数字符串后面，添加(宽度值-字符串长度)个空格字符
            while(len < field_width--) {
                *str++ = ' ';
            }

            break;
        // 如果格式转换符是'o'，表示需将对应的参数转换成八进制数的字符串
        case 'o':
            str = number(str, va_arg(args, unsigned long), 8,
                         field_width, precision, flags);

            break;
        // 如果格式转换符是'p'，表示对应参数的一个指针类型
        case 'p':
            // 此时若该参数没有设置宽度域，则默认宽度为 8，并且需要添零
            if(field_width == -1) {
                field_width = 8;
                flags |= ZEROPAD;
            }
            str = number(str, (unsigned long)va_arg(args, void *),
                         16, field_width, precision, flags);

            break;
        // 若格式转换指示是 'x' 或 'X',则表示对应参数需要打印成十六进制数输出
        case 'x':
            flags |= SMALL;
        case 'X':
            str = number(str, va_arg(args, unsigned long), 16,
                         field_width, precision, flags);

            break;
        // 如果格式转换字符是'd', 'i' 或 'u'，则表示对应参数是整数
        case 'd':
        case 'i':
            // 'd', 'i'代表符号整数，因此需要加上带符号标志
            flags |= SIGN;
        // 'u'代表无符号整数
        case 'u':
            str = number(str, va_arg(args, unsigned long), 10,
                         field_width, precision, flags);

            break;
        // 若格式转换指示符是 'n'
        // 表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中
        case 'n':
            // 首先利用 va_arg()取得该参数指针
            ip = va_arg(args, int *);
            // 然后将已经转换好的字符数存入该指针所指的位置
            *ip = (str - buf);

            break;
        // 如果是'%'，则输出一个%
        case '%':
            *str++ = '%';

            break;
        // 剩余情况
        default:
        // 若格式转换符不是 '%'，则表示格式字符串有错
            if(*fmt != '%') {
                // 直接将一个'%'写入输出串中
                *str++ = '%';
            }
            // 如果格式转换符的位置处还有字符，则也直接将该字符写入输出串中
            // 然后继续循环处理格式字符串
            if(*fmt) {
                *str++ = *fmt;
            } else {
                // 否则表示已经处理到格式字符串的结尾处，则退出循环
                --fmt;
            }

            break;
        }
    }
    // 最后在转换好的字符串结尾处添上字符串结束标志
    *str = '\0';

    // 返回转换好的字符串长度值
    i = str - buf;
    // 断言检查
    assert(i < 1024);

    return i;
}

// 结果按照格式输出字符串到buf缓冲区中
// 返回转换好的字符串长度
int sprintf(char *buf, const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    int i = vsprintf(buf, fmt, args);

    va_end(args);

    return i;
}